﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Kurax.BoardGames.Client.Engine.Commands;

namespace Kurax.BoardGames.Client.Engine {
    public class Network {
        private const int POOL_CAPACITY = 16;
        private readonly Socket socket;
        private readonly Queue<SocketAsyncEventArgs> socketAsyncPool;
        private readonly Dictionary<short, CommandBase> commandPool;
        private AutoResetEvent receiveWaitHandle;
        private short transactionId = short.MinValue;
        public event EventHandler ConnectComplete;

        public bool IsConnected {
            get;
            private set;
        }

        public bool LastOperationSuccess {
            get;
            private set;
        }

        public SocketError LastOperationError {
            get;
            private set;
        }

        public string LastOperationMessage {
            get;
            private set;
        }

        private SocketAsyncEventArgs GetSocketAsyncEventArgs(object token = null) {
            SocketAsyncEventArgs args = null;
            lock(this.socketAsyncPool)
                if(this.socketAsyncPool.Count > 0)
                    args = this.socketAsyncPool.Dequeue();
            if(args == null) {
                args = new SocketAsyncEventArgs();
                args.Completed += this.SocketAsyncEventArgs_Completed;
            }
            args.UserToken = token;
            return args;
        }

        private void PutSocketAsyncEventArgs(SocketAsyncEventArgs args) {
            lock(this.socketAsyncPool)
                if(this.socketAsyncPool.Count < POOL_CAPACITY)
                    this.socketAsyncPool.Enqueue(args);
        }

        private void ProcessAsyncEventArgs(SocketAsyncEventArgs args) {
            this.LastOperationSuccess = args.SocketError == SocketError.Success;
            this.LastOperationError = args.SocketError;
            this.LastOperationMessage = null;
            switch(args.SocketError) {
                case SocketError.Success:
                    this.LastOperationMessage = "Socket 操作成功。";
                    break;
                case SocketError.SocketError:
                    this.LastOperationMessage = "发生了未指定的 Socket 错误。";
                    break;
                case SocketError.Interrupted:
                    this.LastOperationMessage = "已取消阻止 Socket 调用的操作。";
                    break;
                case SocketError.AccessDenied:
                    this.LastOperationMessage = "已试图通过被其访问权限禁止的方式访问 Socket。";
                    break;
                case SocketError.Fault:
                    this.LastOperationMessage = "基础套接字提供程序检测到无效的指针地址。";
                    break;
                case SocketError.InvalidArgument:
                    this.LastOperationMessage = "给 Socket 成员提供了一个无效参数。";
                    break;
                case SocketError.TooManyOpenSockets:
                    this.LastOperationMessage = "基础套接字提供程序中打开的套接字太多。";
                    break;
                case SocketError.WouldBlock:
                    this.LastOperationMessage = "对非阻止性套接字的操作不能立即完成。";
                    break;
                case SocketError.InProgress:
                    this.LastOperationMessage = "阻止操作正在进行中。";
                    break;
                case SocketError.AlreadyInProgress:
                    this.LastOperationMessage = "非阻止性 Socket 已有一个操作正在进行中。";
                    break;
                case SocketError.NotSocket:
                    this.LastOperationMessage = "对非套接字尝试 Socket 操作。";
                    break;
                case SocketError.DestinationAddressRequired:
                    this.LastOperationMessage = "在对 Socket 的操作中省略了必需的地址。";
                    break;
                case SocketError.MessageSize:
                    this.LastOperationMessage = "数据报太长。";
                    break;
                case SocketError.ProtocolType:
                    this.LastOperationMessage = "此 Socket 的协议类型不正确。";
                    break;
                case SocketError.ProtocolOption:
                    this.LastOperationMessage = "对 Socket 使用了未知、无效或不受支持的选项或级别。";
                    break;
                case SocketError.ProtocolNotSupported:
                    this.LastOperationMessage = "未实现或未配置协议。";
                    break;
                case SocketError.SocketNotSupported:
                    this.LastOperationMessage = "在此地址族中不存在对指定的套接字类型的支持。";
                    break;
                case SocketError.OperationNotSupported:
                    this.LastOperationMessage = "协议族不支持地址族。";
                    break;
                case SocketError.ProtocolFamilyNotSupported:
                    this.LastOperationMessage = "未实现或未配置协议族。";
                    break;
                case SocketError.AddressFamilyNotSupported:
                    this.LastOperationMessage = "不支持指定的地址族。如果指定了 IPv6 地址族而未在本地计算机上安装 IPv6 堆栈，则会返回此错误。如果指定了 IPv4 地址族而未在本地计算机上安装 IPv4 堆栈，则会返回此错误。";
                    break;
                case SocketError.AddressAlreadyInUse:
                    this.LastOperationMessage = "通常，只允许使用地址一次。";
                    break;
                case SocketError.AddressNotAvailable:
                    this.LastOperationMessage = "选定的 IP 地址在此上下文中无效。";
                    break;
                case SocketError.NetworkDown:
                    this.LastOperationMessage = "网络不可用。";
                    break;
                case SocketError.NetworkUnreachable:
                    this.LastOperationMessage = "不存在到远程主机的路由。";
                    break;
                case SocketError.NetworkReset:
                    this.LastOperationMessage = "应用程序试图在已超时的连接上设置选项。";
                    break;
                case SocketError.ConnectionAborted:
                    this.LastOperationMessage = "此连接由 .NET Framework 或基础套接字提供程序中止。";
                    break;
                case SocketError.ConnectionReset:
                    this.LastOperationMessage = "此连接由远程对等计算机重置。";
                    break;
                case SocketError.NoBufferSpaceAvailable:
                    this.LastOperationMessage = "没有可用于 Socket 操作的可用缓冲区空间。";
                    break;
                case SocketError.IsConnected:
                    this.LastOperationMessage = "Socket 已连接。";
                    break;
                case SocketError.NotConnected:
                    this.LastOperationMessage = "应用程序试图发送或接收数据，但是 Socket 未连接。";
                    break;
                case SocketError.Shutdown:
                    this.LastOperationMessage = "发送或接收数据的请求未得到允许，因为 Socket 已被关闭。";
                    break;
                case SocketError.TimedOut:
                    this.LastOperationMessage = "连接尝试超时，或者连接的主机没有响应。";
                    break;
                case SocketError.ConnectionRefused:
                    this.LastOperationMessage = "远程主机正在主动拒绝连接。";
                    break;
                case SocketError.HostDown:
                    this.LastOperationMessage = "由于远程主机被关闭，操作失败。";
                    break;
                case SocketError.HostUnreachable:
                    this.LastOperationMessage = "没有到指定主机的网络路由。";
                    break;
                case SocketError.ProcessLimit:
                    this.LastOperationMessage = "正在使用基础套接字提供程序的进程过多。";
                    break;
                case SocketError.SystemNotReady:
                    this.LastOperationMessage = "网络子系统不可用。";
                    break;
                case SocketError.VersionNotSupported:
                    this.LastOperationMessage = "基础套接字提供程序的版本超出范围。";
                    break;
                case SocketError.NotInitialized:
                    this.LastOperationMessage = "尚未初始化基础套接字提供程序。";
                    break;
                case SocketError.Disconnecting:
                    this.LastOperationMessage = "正常关机正在进行中。";
                    break;
                case SocketError.TypeNotFound:
                    this.LastOperationMessage = "未找到指定的类。";
                    break;
                case SocketError.HostNotFound:
                    this.LastOperationMessage = "无法识别这种主机。该名称不是正式的主机名或别名。";
                    break;
                case SocketError.TryAgain:
                    this.LastOperationMessage = "无法解析主机名。请稍后重试。";
                    break;
                case SocketError.NoRecovery:
                    this.LastOperationMessage = "错误不可恢复或找不到请求的数据库。";
                    break;
                case SocketError.NoData:
                    this.LastOperationMessage = "在名称服务器上找不到请求的名称或 IP 地址。";
                    break;
                case SocketError.IOPending:
                    this.LastOperationMessage = "应用程序已启动一个无法立即完成的重叠操作。";
                    break;
                case SocketError.OperationAborted:
                    this.LastOperationMessage = "由于 Socket 已关闭，重叠的操作被中止。";
                    break;
            }
        }

        private void StartListening() {
            var buffer = new byte[this.socket.ReceiveBufferSize];
            while(true) {
                if(!this.IsConnected)
                    break;

                var args = this.GetSocketAsyncEventArgs();
                args.SetBuffer(buffer, 0, buffer.Length);
                this.socket.ReceiveAsync(args);
                this.receiveWaitHandle.WaitOne();
            }
        }

        private void ProcessConnectAsyncEventArgs() {
            this.IsConnected = this.socket.Connected;
            if(this.IsConnected) {
                this.receiveWaitHandle = new AutoResetEvent(false);
                var thread = new Thread(this.StartListening);
                thread.Start();
            }
            this.RaiseConnectComplete();
        }

        private void ProcessSendAsyncEventArgs(SocketAsyncEventArgs args) {
            var command = (CommandBase)args.UserToken;
            lock(this.commandPool)
                this.commandPool[command.TransactionId] = command;
        }

        private void ProcessReceiveAsyncEventArgs(SocketAsyncEventArgs args) {
            this.receiveWaitHandle.Set();
            var data = args.Buffer.Take(args.BytesTransferred).ToArray();
            if(data.Length <= 8 || data[0] != 2 || data[data.Length - 1] != 3)
                return;

            var txnId = BitConverter.ToInt16(data, 1);
            lock(this.commandPool)
                if(!this.commandPool.ContainsKey(txnId))
                    return;

            CommandBase command;
            lock(this.commandPool) {
                command = this.commandPool[txnId];
                this.commandPool.Remove(txnId);
            }
            command.OnServerResponsed(data.Skip(3).Take(data.Length - 4).ToArray());
        }

        private void RaiseConnectComplete() {
            var handler = this.ConnectComplete;
            if(handler != null)
                handler(this, EventArgs.Empty);
        }

        private short GetTransactionId() {
            if(++this.transactionId == short.MaxValue)
                this.transactionId = short.MinValue;
            return this.transactionId;
        }

        private void SocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) {
            this.ProcessAsyncEventArgs(e);
            switch(e.LastOperation) {
                case SocketAsyncOperation.Connect:
                    this.ProcessConnectAsyncEventArgs();
                    break;
                case SocketAsyncOperation.Send:
                    this.ProcessSendAsyncEventArgs(e);
                    break;
                case SocketAsyncOperation.Receive:
                    this.ProcessReceiveAsyncEventArgs(e);
                    break;
            }
            this.PutSocketAsyncEventArgs(e);
        }

        public Network() {
            this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            this.socketAsyncPool = new Queue<SocketAsyncEventArgs>(POOL_CAPACITY);
            this.commandPool = new Dictionary<short, CommandBase>();
        }

        public void Connect(string host = "localhost", int port = 4533) {
            if(this.IsConnected) {
                var ep = (DnsEndPoint)this.socket.RemoteEndPoint;
                throw new InvalidOperationException(string.Format("Already connected to {0}:{1}", ep.Host, ep.Port));
            }

            var args = this.GetSocketAsyncEventArgs();
            args.SocketClientAccessPolicyProtocol = SocketClientAccessPolicyProtocol.Tcp;
            args.RemoteEndPoint = new DnsEndPoint(host, port);
            if(!this.socket.ConnectAsync(args))
                this.ProcessConnectAsyncEventArgs();
        }

        public void Disconnect() {
            if(!this.IsConnected)
                return;
            this.IsConnected = false;
            this.socket.Shutdown(SocketShutdown.Both);
            this.socket.Close();
        }

        public void SendCommand(CommandBase command) {
            if(command == null)
                throw new ArgumentNullException("command");
            if(!this.IsConnected)
                throw new InvalidOperationException("Can't send command, server not connected.");

            var data = new List<byte> {
                0x2
            };
            data.AddRange(BitConverter.GetBytes(command.TransactionId = this.GetTransactionId()));
            data.AddRange(BitConverter.GetBytes(command.CommandId));
            data.AddRange(command.GetBodyData());
            data.Add(0x3);

            var args = this.GetSocketAsyncEventArgs(command);
            args.SetBuffer(data.ToArray(), 0, data.Count);
            command.SendTime = DateTime.Now;
            if(!this.socket.SendAsync(args))
                this.ProcessSendAsyncEventArgs(args);
        }
    }
}
